package com.googlecode.dgwt.client.dijit.theme;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.ui.Widget;
import com.googlecode.dgwt.client.dojo.Dojo;

/**
 * Represents a fragment of Cascade Style Sheet 
 * declaration for a specific Dijit Widget
 * 
 * @author wellsantos@wellsantos.com
 * @since 0.01
 */
public abstract class CSS {

    private String baseClass;
    private Set<CSS> superClasses;
    private Set<CSS> requiredClasses;
    private String declarationLTR = "";
    private String declarationRTL = "";
    private boolean published;

    /**
     * Private constructor
     */
    private CSS(){
        superClasses = new LinkedHashSet<CSS>();
        requiredClasses = new LinkedHashSet<CSS>();
        baseClass = "";
    }

    /**
     * Creates a new CSS declaration
     * 
     * @param widgetBaseClass
     *            name of base-class used in this declaration
     */
    protected CSS(String widgetBaseClass){
        this();
        this.baseClass = widgetBaseClass;
    }
    
    /**
     * Gets the name of base-class used in this CSS
     * declaration and import it to be used adequately
     * 
     * @return String
     */
    public String getBaseClass() {
        return baseClass;
    }

    /**
     * Gets a list of CSS used as super classes by this one.
     * 
     * @return {@link Set} of CSS
     */
    public Set<CSS> getSuperClasses(){
        return Collections.unmodifiableSet(superClasses);
    }

    /**
     * Gets the a list CSS required by this one.
     * 
     * @return {@link Set} of CSS
     */
    public Set<CSS> getRequiredClasses(){
        return Collections.unmodifiableSet(requiredClasses);
    }

    /**
     * Adds one or more CSS as a super class of this. 
     * 
     * @param superClasses CSS array
     */
    public void addSuperClass(CSS ...superClasses) {
        for (CSS css : superClasses){
            this.superClasses.add(css);
            this.requiredClasses.add(css);
        }
    }

    /**
     * Adds one or more CSS as a required class by this.
     *  
     * @param requiredClasses CSS array
     */
    public void addRequiredClass(CSS ...requiredClasses) {
        for (CSS css : requiredClasses){
            this.requiredClasses.add(css);
        }
    }

    /**
     * Applies this CSS on a {@link Widget} by adding the necessary style names
     * and importing this declaration into the HTML document to be used adequately.
     * 
     * @param widget
     *            list of widgets where to apply this CSS
     */
    public void applyTo(Widget ...widgets){
        for (Widget widget : widgets){
            applyTo(widget.getElement());
        }
    }

    /**
     * Applies this CSS on a {@link Widget} by adding the necessary style names
     * and importing this declaration into the HTML document to be used
     * adequately.
     * 
     * @param widget
     *            the widget where to apply this CSS
     * @param setAsStylePrimary
     *            if True will set this CSS {@link #getBaseClass() baseClass} as
     *            the primary style name for this Widget
     */
    public void applyTo(Widget widget, boolean setAsStylePrimary){
        applyTo(widget.getElement());
        if (setAsStylePrimary && !isAnonymous()){
            widget.setStylePrimaryName(baseClass);
        }
    }
    
    /**
     * Applies this CSS on a {@link Element} by adding the necessary style names
     * and importing this declaration into the HTML document to be used adequately.
     * 
     * @param element
     *            list of elements where to apply this CSS
     */
    public void applyTo(Element ...elements){
        for (Element element : elements){
            applyTo(element);
        }
    }
    
    /**
     * Applies this CSS on a {@link Element} by adding the necessary style names
     * and importing this declaration into the HTML document to be used adequately.
     * 
     * @param element
     *            the element where to apply this CSS
     */
    public void applyTo(Element element){
        applyToAux(element, this);
        Dojo.addClass(element, baseClass);
        importDeclaration();
    }
    
    private void applyToAux(Element element, CSS target){
        for (CSS classe : superClasses){
            if (classe != target){
                classe.applyToAux(element, target);
                Dojo.addClass(element, classe.baseClass);
            }
        }    
    }
    
    /**
     * Imports the declaration into the HTML document 
     */
    public void importDeclaration(){
        if (!isPublished()){
            // prevents infinity recursion
            published = true;
            
            for (CSS classe : requiredClasses){
                classe.importDeclaration();
            }
            
            published = publish(declarationLTR, declarationRTL);            
        }
    }

    /**
     * Appends a CSS fragment at end of the current
     * 
     * @param declarationLTR LTR CSS declaration
     */
    public void addDeclaration(String declarationLTR) {
        addDeclaration(declarationLTR, "");
    }
    
    /**
     * Appends a CSS fragment at end of the current
     * 
     * @param declarationLTR LTR CSS declaration
     * @param declarationRTL RTL CSS declaration
     */
    public void addDeclaration(String declarationLTR, String declarationRTL) {
        if (isPublished()){
            publish(declarationLTR, declarationRTL);
        }
        this.declarationLTR += declarationLTR == null ? "" : declarationLTR;
        this.declarationRTL += declarationRTL == null ? "" : declarationRTL;
    }

    /**
     * Gets the LTR declaration of this CSS
     * @return CSS text
     */
    public String getDeclarationLTR() {
        return declarationLTR;
    }

    /**
     * Gets the RTL declaration of this CSS
     * @return CSS text
     */
    public String getDeclarationRTL() {
        return declarationRTL;
    }

    /**
     * Replace the declaration of this CSS
     * 
     * @param declarationLTR LTR CSS declaration
     */
    public void setDeclaration(String declarationLTR) {
        setDeclaration(declarationLTR, "");
    }
    
    /**
     * Replace the declaration of this CSS
     * 
     * @param declarationLTR LTR CSS declaration
     * @param declarationRTL RTL CSS declaration
     */
    public void setDeclaration(String declarationLTR, String declarationRTL) {
        assert (!isPublished()) : "CSS already published";
        
        this.declarationLTR = declarationLTR == null ? "" : declarationLTR;
        this.declarationRTL = declarationRTL == null ? "" : declarationRTL;
    }

    /**
     * Return TRUE if the declaration of this CSS is empty
     * 
     * @return boolean
     */
    public boolean isEmpty(){
        return declarationLTR.length() == 0
            && declarationRTL.length() == 0
            && !isPublished();
    }
    
    /**
     * Return True if this CSS is anonymous (don't have a base-class).
     * 
     * @return boolean 
     */
    public boolean isAnonymous(){
        return baseClass.isEmpty();
    }

    /**
     * Return True if the declaration of this CSS was already published
     * 
     * @return boolean
     */
    public final boolean isPublished(){
        return published;
    }

    /**
     * Define this CSS as published or not.
     * 
     * @param published True/False
     */
    protected void setPublished(boolean published) {
        this.published = published;
    }

    /**
     * Publish the declaration into the HTML document.
     * 
     * @param declarationLTR
     *            LTR CSS text
     * 
     * @param declarationRTL
     *            RTL CSS text
     * 
     * @return True if it was successful published
     */
    protected abstract boolean publish(String declarationLTR, String declarationRTL);

    /**
     * An empty CSS implementation
     */
    public static final CSS EMPTY_CSS = new CSS(){
        @Override
        public boolean publish(String declarationLTR, String declarationRTL) {
            return true;
        }
    };

}
